Skip to content

v2.0: Modernization (M1-M6, 44 tasks)#374

Draft
etr wants to merge 113 commits intomasterfrom
feature/v2.0
Draft

v2.0: Modernization (M1-M6, 44 tasks)#374
etr wants to merge 113 commits intomasterfrom
feature/v2.0

Conversation

@etr
Copy link
Copy Markdown
Owner

@etr etr commented Apr 30, 2026

Summary

Integration branch for the v2.0 modernization effort. Tasks land here individually (one merge commit per task) so the full v2.0 ships as a single reviewable PR.

This PR will remain draft until all milestones are complete.

Milestones

  • M1 — Foundation (TASK-001 … TASK-007): toolchain + build-system prerequisites
  • M2 — Response (TASK-008 … TASK-013)
  • M3 — Request (TASK-014 … TASK-020)
  • M4 — Handlers (TASK-021 … TASK-026)
  • M5 — Routing & Lifecycle (TASK-027 … TASK-036)
  • M6 — Release (TASK-037 … TASK-044)

Specs live under specs/ (product_specs, architecture, tasks).

Merged tasks

  • TASK-001 — Bump C++ standard floor to C++20

Test plan

Per-task validation runs through the groundwork validation loop on each task branch before merging here. Pre-merge of v2.0 to master:

  • ./configure && make clean on macOS (Apple Clang) and Linux (recent GCC)
  • make check green
  • CI matrix green across all supported toolchains
  • No -std=c++(11|14|17) regressions in tree
  • ChangeLog and README reflect v2.0 changes

🤖 Generated with Claude Code

etr and others added 3 commits April 30, 2026 23:24
Raises the project's C++ standard floor from C++17 to C++20 so that
subsequent v2.0 work can rely on concepts, std::span, <bit>,
designated initializers, and std::pmr without per-feature gates.

- m4/ax_cxx_compile_stdcxx.m4: replaced with upstream serial 25
  (autoconf-archive). The vendored serial 12 only accepted [11], [14],
  [17] and m4_fatals on anything else; serial 25 adds [20] and [23]
  alternatives plus the C++20 feature-test bodies.
- configure.ac:47: AX_CXX_COMPILE_STDCXX([17]) -> ([20], [noext],
  [mandatory]). [noext] keeps -std=c++20 (no gnu++20 extensions in
  ABI surface); [mandatory] aborts cleanly on too-old toolchains.
- configure.ac:224: dropped redundant -std=c++17 from the
  --enable-debug AM_CXXFLAGS branch. AX_CXX_COMPILE_STDCXX already
  appends -std=c++20 to $CXX, so leaving the override in would
  silently downgrade debug builds.
- Verified Makefile.am, src/Makefile.am, test/Makefile.am, and
  examples/Makefile.am: no per-subdirectory -std= overrides exist.
- .github/workflows/verify-build.yml:
  - Pruned gcc-9, clang-11, clang-12 matrix rows (incomplete C++20
    support: missing concepts/<bit>/<span> in libstdc++/libc++).
  - Bumped IWYU CXXFLAGS from -std=c++11 to -std=c++20.
- README.md: bumped Requirements to "g++ >= 10 or clang >= 13
  (Apple Clang from Xcode 15+)" and "C++20 or newer". Added a
  one-liner about gcc-toolset-14 on RHEL 9.
- README.CentOS-7: updated to reflect the C++20 floor and the
  gcc-toolset-14 workaround.
- ChangeLog: noted the standard bump under 0.20.0.

Verification (Apple Clang 21 on macOS):
- ./configure && make: succeeds with -std=c++20.
- make check: 17/17 tests pass.
- ./configure --enable-debug && make: clean under
  -Wall -Wextra -Werror -pedantic -std=c++20.
- make check (debug): 17/17 tests pass.
- grep -RE '-std=(c\+\+11|c\+\+14|c\+\+17|gnu\+\+(11|14|17))'
    configure.ac Makefile.am src test  -> zero matches.

Refs: PRD §2 NFR (modern C++ idioms), DR-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First task in the v2.0 milestone series (M1-Foundation). Raises the
project's C++ standard floor from C++17 to C++20.
Local planning artifacts from groundwork task scaffolding shouldn't be tracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.58%. Comparing base (d8b055e) to head (cf31239).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #374      +/-   ##
==========================================
+ Coverage   68.03%   69.58%   +1.55%     
==========================================
  Files          34       30       -4     
  Lines        1730     2647     +917     
  Branches      697      983     +286     
==========================================
+ Hits         1177     1842     +665     
- Misses         80      158      +78     
- Partials      473      647     +174     
Files with missing lines Coverage Δ
src/create_test_request.cpp 50.00% <ø> (ø)
src/detail/body.cpp 89.06% <ø> (ø)
src/detail/http_endpoint.cpp 60.00% <ø> (ø)
src/http_request.cpp 63.79% <ø> (-5.18%) ⬇️
src/http_resource.cpp 100.00% <ø> (+83.33%) ⬆️
src/http_response.cpp 82.35% <ø> (+18.35%) ⬆️
src/http_utils.cpp 66.97% <ø> (ø)
src/httpserver/create_webserver.hpp 96.98% <ø> (ø)
src/httpserver/detail/body.hpp 93.47% <ø> (ø)
src/httpserver/detail/http_endpoint.hpp 85.00% <ø> (ø)
... and 15 more

... and 5 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d8b055e...cf31239. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

etr and others added 26 commits May 1, 2026 00:49
Tighten the public/private header split so detail headers and the
HTTPSERVER_COMPILATION macro cannot leak to downstream consumers, and
add make-check assertions that protect the surface going forward.

Changes:
  - src/httpserver.hpp: #undef _HTTPSERVER_HPP_INSIDE_ after all child
    includes so the macro does not survive into a consumer's TU.
  - src/Makefile.am: move httpserver/details/http_endpoint.hpp out of
    nobase_include_HEADERS into noinst_HEADERS — distributed in the
    tarball but never installed under $prefix/include. Add
    -DHTTPSERVER_COMPILATION to AM_CPPFLAGS so the lib's own TUs see it.
  - test/Makefile.am: add -DHTTPSERVER_COMPILATION to AM_CPPFLAGS so
    first-party unit tests that legitimately include detail headers
    still compile.
  - configure.ac: stop injecting -DHTTPSERVER_COMPILATION into global
    CXXFLAGS. Scope is now per-directory (lib + tests only); examples
    build as true consumers via <httpserver.hpp>.
  - Makefile.am: new check-headers target with four sub-checks
    (A.1 direct public include must fail, A.2 direct detail include
    must fail, A.3 umbrella must compile cleanly, A.4 post-umbrella
    direct include must still fail) and a new check-install-layout
    target that runs `make install DESTDIR=...` to a stage and asserts
    no `details/` directory or `*_impl.hpp` file leaks. Both wired into
    check-local.
  - test/headers/: four one-line consumer TUs driving the checks.

Per the plan's Phase 3a-i, the detail-header gate stays dual-mode
(_HTTPSERVER_HPP_INSIDE_ || HTTPSERVER_COMPILATION) because
webserver.hpp still transitively includes details/http_endpoint.hpp;
TASK-014's PIMPL split will let a future change tighten that gate to
HTTPSERVER_COMPILATION-only.

Acceptance criteria verified:
  - 17/17 existing tests pass under release and --enable-debug.
  - check-headers A.1 fires with the gate error string.
  - check-install-layout: staged install has no details/ and no
    *_impl.hpp; httpserver.hpp + httpserverpp symlink installed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… for MSYS

TASK-001 raised the C++ floor to C++20, which broke matrix entries
running gcc-10, clang-14, and clang-15 (the autoconf C++20 feature
test rejects them). Drop those entries from extra/none, and bump the
lint and performance jobs (which were pinned to gcc-10) to gcc-14 so
they still exercise an older-but-supported toolchain.

The MSYS native job started failing with "microhttpd.h not found"
because the runner image no longer ships libmicrohttpd transitively.
Add libmicrohttpd-devel to the explicit pacman install line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…set check

libmicrohttpd's <microhttpd.h> hard-asserts that _SYS_TYPES_FD_SET is
defined on Cygwin/MSYS, otherwise emitting `#error Cygwin with winsock
fd_set is not supported`. newlib defines that macro via <sys/select.h>,
included from <sys/types.h> only when __BSD_VISIBLE -- which in turn is
gated on _DEFAULT_SOURCE. Strict ANSI C++ (-std=c++NN, the floor we
adopted in TASK-001 with AX_CXX_COMPILE_STDCXX noext) suppresses
newlib's auto-define of _DEFAULT_SOURCE, so the macro never lands and
microhttpd.h refuses to compile.

This is unrelated to the C++ language mode -- _DEFAULT_SOURCE only
controls feature-test gating in system headers -- so defining it here
preserves DR-001's "noext" portability promise while fixing the build
on every Cygwin/MSYS consumer (not just our CI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n PPA, revert MSYS libmicrohttpd-devel

Three small follow-ups now that the _DEFAULT_SOURCE Cygwin/MSYS fix has
landed:

1. The four test/headers/consumer_*.cpp gate tests added in TASK-002
   were missing the project's standard LGPL/copyright header, tripping
   the lint job once gcc-14 was running cpplint over them.

2. The "Install Ubuntu test sources" step was running
   add-apt-repository ppa:ubuntu-toolchain-r/test which talks to
   launchpad and has been hitting 504 Gateway Time-out across runs.
   With the C++20 floor we no longer need the toolchain PPA -- gcc-11
   through gcc-14 ship in stock ubuntu-22.04/24.04 repos, and
   clang-13/16-18 likewise. Keep just apt-get update.

3. The earlier "add libmicrohttpd-devel to MSYS pacman" attempt was
   wrong -- there is no such MSYS native package. The actual fix was
   the configure.ac _DEFAULT_SOURCE define landed in 5b78014; revert
   the bogus pacman entry so the install step stops failing first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce a new public header `src/httpserver/feature_unavailable.hpp`
defining `class feature_unavailable : public std::runtime_error`. The
constructor takes `(std::string_view feature, std::string_view
build_flag)` and composes a `what()` message that names both, e.g.
`"feature 'tls' unavailable: built without HAVE_GNUTLS"`.

The class is header-only and inline. It has no library dependencies
(only <stdexcept>, <string>, <string_view>), so any TU — including
later tasks like TASK-034 that need to throw it from sites in
build-time-disabled code paths — can include it without circular
header coupling. Keeping it inline also avoids ABI churn for what is
effectively a labelled std::runtime_error and keeps libhttpserver_la
sources untouched.

The header is re-exported from the umbrella `<httpserver.hpp>`
unconditionally (no `#ifdef HAVE_*` wrap): even a build with no
optional features must let consumers name `feature_unavailable` so
they can write `try { ... } catch (const httpserver::feature_unavailable&)`.

The TASK-002 inclusion gate is applied verbatim — direct inclusion of
the header without the umbrella or `HTTPSERVER_COMPILATION` errors
out, and `_HTTPSERVER_HPP_INSIDE_` does not leak post-umbrella (both
verified by the existing check-headers A.1–A.4 recipes).

A new unit test `test/unit/feature_unavailable_test.cpp` provides:
- a TU-scope `static_assert(std::is_base_of_v<std::runtime_error,
  httpserver::feature_unavailable>)` (acceptance criterion 1),
- a test that catches as `std::runtime_error` and asserts both the
  feature name and the build flag appear in `what()` (AC 2),
- a test that catches as the concrete type and confirms it slices to
  `runtime_error` correctly,
- a test with a different (feature, flag) pair to guard against
  hard-coded message text.

Verified locally:
- `make check`: 18/18 PASS (was 17, +1 for feature_unavailable),
- check-headers A.1–A.4 PASS,
- check-install-layout PASS (no details/ leak),
- staged install ships exactly one feature_unavailable.hpp at
  $(prefix)/include/httpserver/feature_unavailable.hpp,
- debug build (--enable-debug, -Werror -Wextra -pedantic) builds and
  tests cleanly.

Refs: PRD-FLG-REQ-004, PRD-FLG-REQ-005; §7 (feature availability).
Introduces a library-defined POD `httpserver::iovec_entry { const void* base;
std::size_t len; }` in a new public header `<httpserver/iovec_entry.hpp>`,
included by `<httpserver/http_response.hpp>` and the umbrella header. The
type replaces POSIX `struct iovec` at the public API surface, keeping
`<sys/uio.h>` out of every public header.

Layout pinning lives in `src/iovec_response.cpp` as six unconditional
static_asserts: three against POSIX `struct iovec` (size + iov_base /
iov_len offsets) per the spec, and three parallel asserts against
libmicrohttpd `MHD_IoVec` because that is the actual cast target on the
dispatch path. The MHD_IoVec asserts are an addition over the spec —
without them the reinterpret_cast bridge is the unsafe one. A TODO
sentinel comment (LIBHTTPSERVER_TODO_TASK004_MEMCPY_FALLBACK) documents
the memcpy fallback strategy that would activate if a divergent-layout
platform ever trips one of the asserts. Today every supported platform
(glibc, musl, macOS, FreeBSD, NetBSD, OpenBSD, illumos) shares the same
layout so the asserts pass and the reinterpret_cast is well-defined.

`iovec_response::get_raw_response()` now builds a contiguous
`std::vector<iovec_entry>` from its owned std::strings and
reinterpret_casts to `const MHD_IoVec*` when calling MHD. This proves
the cast bridge in production code today; TASK-010 will move the same
line into the future `details/body.hpp` factory.

Two new TDD-driven test programs:
- `test/unit/iovec_entry_test.cpp` — verifies POD traits (standard
  layout, trivially copyable), member types, layout equivalence with
  POSIX `struct iovec` from a consumer perspective, and the
  reinterpret_cast bridge round-trip.
- `test/unit/header_hygiene_iovec_test.cpp` — declares a colliding
  `struct iovec` before including `iovec_entry.hpp` directly. The TU
  compiling at all proves the new public header pulls in nothing from
  `<sys/uio.h>`. (The broader umbrella-leak concern — current umbrella
  transitively pulls `<sys/uio.h>` via gnutls and `<sys/socket.h>` —
  is out of scope for TASK-004 and is the remit of TASK-007's
  header-hygiene CI gate.)

Build: 20/20 tests pass under both default and `--enable-debug`
(-Wall -Wextra -Werror -pedantic -O0). `grep -E '#include\s+<sys/uio\.h>'
src/httpserver/*.hpp` returns no results. `make install` ships the new
header at `$prefix/include/httpserver/iovec_entry.hpp`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vements

- Delete copy constructor and copy assignment on iovec_response to close
  CWE-416 use-after-free: the owning constructor stores entries_ as raw
  void* into owned_buffers_ strings; a defaulted copy would shallow-copy
  entries_ while deep-copying owned_buffers_ to new addresses, making
  entries_ dangle after source destruction. Move semantics are safe and
  kept. Static asserts in iovec_response_test.cpp guard this invariant.

- Remove the spurious '#include "httpserver/iovec_entry.hpp"' from
  http_response.hpp; http_response itself never uses iovec_entry, and
  iovec_response.hpp already includes it directly.

- Add @attention Doxygen contract to the non-owning iovec_response
  constructor documenting that caller buffers must outlive MHD_destroy_response.

- Remove duplicate offsetof/sizeof/alignof layout-pinning static_asserts
  from iovec_entry_test.cpp; authoritative copies live in iovec_response.cpp
  where the reinterpret_cast actually occurs.

- Add iovec_response_test.cpp (was untracked) with content-type forwarding
  tests and move-semantics tests for both constructor variants.

- Commit iovec_response.hpp, iovec_response.cpp, and test/Makefile.am that
  were modified/added in iter-1 but never staged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces the type-safe HTTP-method primitives that http_resource,
the route table, and lambda registration will consume.

- enum class http_method : std::uint8_t { get, head, post, put, del,
  connect, options, trace, patch, count_ }. Identifier `del` avoids
  the C++ keyword; wire token returned by to_string is "DELETE".
- struct method_set { std::uint32_t bits = 0; } with constexpr
  contains/set/clear/set_all/clear_all and defaulted operator==.
- Free constexpr noexcept bitwise operators (|, &, ^, ~, |=, &=, ^=)
  on http_method and method_set, including mixed (set, enum) overloads.
  All operators usable in constant expressions and at runtime ("consteval-
  friendly" without forbidding runtime use, which the route-table writer
  path needs).
- to_string(http_method) returning std::string_view for logging and
  the 405 Allow: header. Total over the 9 enumerators; out-of-range
  returns an empty view so logging stays robust against stale values.
- Layout/width invariants pinned at namespace scope:
  count_ <= 32, standard layout, trivially copyable,
  sizeof(method_set) == sizeof(uint32_t).
- Re-exported from <httpserver.hpp> and installed via
  nobase_include_HEADERS in src/Makefile.am.
- Test driver test/unit/http_method_test.cpp covers both compile-time
  static_asserts (round-trip, layout, bitwise composition, complement
  bounding, to_string totality) and 13 runtime LT_BEGIN_AUTO_TEST
  cases including a contract check that to_string matches
  libmicrohttpd's MHD_HTTP_METHOD_* tokens.

All 22 testsuite entries pass under the default build and under
--enable-debug (-Wall -Wextra -Werror -pedantic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move every value-form #define from public headers into
inline constexpr declarations under httpserver::constants:

- DEFAULT_WS_PORT      -> std::uint16_t (9898)
- DEFAULT_WS_TIMEOUT   -> int           (180 seconds)
- DEFAULT_MASK_VALUE   -> std::uint16_t (0xFFFF)
- NOT_FOUND_ERROR      -> std::string_view ("Not Found")
- METHOD_ERROR         -> std::string_view ("Method not Allowed")
- NOT_METHOD_ERROR     -> std::string_view ("Method not Acceptable")
- GENERIC_ERROR        -> std::string_view ("Internal Error")

The new header src/httpserver/constants.hpp uses the established
two-token gate (_HTTPSERVER_HPP_INSIDE_ + HTTPSERVER_COMPILATION),
is re-exported from <httpserver.hpp>, and is registered in
nobase_include_HEADERS so it ships in the install layout.

Internal callers in webserver.cpp, http_utils.cpp,
create_webserver.hpp, and http_utils.hpp are migrated to the
namespaced names. The string_response call sites materialize a
std::string from the string_view to satisfy the existing ctor
signature.

A new unit test (test/unit/constants_test.cpp) pins the values
and types via static_assert, and uses #ifdef sentinels to
witness that the v1 macro names no longer leak into consumer
namespace after #include <httpserver.hpp>.

NOT_METHOD_ERROR has no in-tree caller; retained for v1 API
parity per the v2.0 mechanical-migration policy.

Acceptance:
- 23/23 tests pass (release + debug -Werror -Wall -Wextra)
- Filtered grep on src/httpserver/*.hpp shows no leftover
  value-constant #defines (include guards, _WINDOWS,
  _WIN32_WINNT, and COMPARATOR are out of scope per plan §2)
- Installed-header layout includes httpserver/constants.hpp

Closes TASK-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark all five action items complete and set task status to Complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update specs/tasks/_index.md to change TASK-006 status from 'In Progress'
to 'Done', matching the completed state in TASK-006.md and the pattern
used by TASK-003, TASK-004, and TASK-005.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a two-layer header-hygiene gate that locks in the "no backend
headers leak through <httpserver.hpp>" invariant from PRD-HDR-REQ-001..003.

Layer 1 -- compile/runtime sentinel (test/unit/header_hygiene_test.cpp):
  Includes only <httpserver.hpp>, then checks well-known include-guard
  macros (MHD_VERSION, _PTHREAD_H{,_}, GNUTLS_GNUTLS_H, _SYS_SOCKET_H{,_},
  _SYS_UIO_H{,_}). At runtime it prints the leaked headers and exits 1.
  Per-target CPPFLAGS overrides AM_CPPFLAGS so HTTPSERVER_COMPILATION
  and the build-tree -I src/httpserver/ entries are NOT in scope --
  mimics a real consumer translation unit.

Layer 2 -- preprocessor grep against staged install (`make check-hygiene`):
  Stages `make install DESTDIR=$(CHECK_HYGIENE_STAGE)` to a clean tree,
  preprocesses test/headers/consumer_umbrella_no_backend.cpp using ONLY
  -I$(CHECK_HYGIENE_STAGE)$(includedir), then greps cpp line markers
  for forbidden backend headers. HEADER_HYGIENE_STRICT controls
  fatality (default no -> informational; yes -> hard fail at TASK-020).

Both gates are wired into `make check`:
- header_hygiene runs as a check_PROGRAMS test, marked XFAIL_TESTS
  until M5 lands and the umbrella is clean. Automake's XPASS-as-error
  default is the explicit signal for TASK-020 to remove the marker.
- check-hygiene runs via check-local; in non-strict mode it prints an
  EXPECTED-FAIL banner with diagnostics and exits 0 so `make check`
  stays green during M2-M5 while keeping leak progress visible.

CI surface: new header-hygiene matrix entry in verify-build.yml runs
`make check-hygiene` as a focused, named GitHub Actions check.

TASK-020.md updated with explicit M5 close-out steps (delete
XFAIL_TESTS line + flip HEADER_HYGIENE_STRICT default).

Verified locally on macOS/aarch64 with gnutls 3.x, libmicrohttpd 1.0.5,
Apple Clang 15+: 24 tests / 23 PASS / 1 XFAIL (header_hygiene); the
sentinel correctly reports microhttpd, pthread, gnutls, sys/socket,
sys/uio leaks; check-hygiene reports EXPECTED-FAIL on staged install
(webserver.hpp still references private detail header until TASK-014).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… hygiene CI matrix

- check-local: build one DESTDIR=.shared-check-stage and pass it to both
  check-install-layout and check-hygiene via CHECK_*_SHARED=yes, halving
  the install cost of `make check`. Standalone invocations still do their
  own install.
- check-hygiene: gate the staged install behind a $(HYGIENE_STAMP) mtime
  sentinel so repeated standalone runs are no-ops when public headers
  haven't changed; bypassed when CHECK_HYGIENE_SHARED=yes.
- check-hygiene grep: anchor HEADER_HYGIENE_FORBIDDEN to a leading "/"
  so leak detection only matches absolute paths, not arbitrary substrings.
- clean-local: remove the stage directories on `make clean`.
- CI: header-hygiene matrix entry skips the unconditional `make check`
  step (the dedicated `make check-hygiene` step is the gate for that job).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the polymorphic body hierarchy that http_response's SBO buffer will
host (TASK-009) and the public body_kind enum that http_response::kind()
will return (TASK-011). TASK-008 ships only the standalone hierarchy:
each subclass is independently constructible, destructible, and
materializable, mirroring the corresponding v1 *_response::get_raw_response.

New public header (umbrella-included):
- httpserver/body_kind.hpp: enum class body_kind : std::uint8_t {
  empty, string, file, iovec, pipe, deferred }; empty=0 so a
  value-initialised body_kind matches the no-body state.

New private header (HTTPSERVER_COMPILATION-only, never installed):
- httpserver/details/body.hpp: abstract detail::body + 6 final
  subclasses (empty_body, string_body, file_body, iovec_body, pipe_body,
  deferred_body) plus per-subclass static_assert(sizeof <= 64) and
  static_assert(alignof(deferred_body) <= 16) for the SBO budget
  (DR-005).

Out-of-line definitions in src/details/body.cpp:
- materialize() per subclass mirrors v1 byte-for-byte
  (string=PERSISTENT, file=open/fstat/lseek/from_fd, iovec=CWE-190
  guard + reinterpret_cast to MHD_IoVec, pipe=from_pipe, deferred=
  from_callback with a static trampoline).
- Layout-pinning static_asserts duplicated from iovec_response.cpp
  (TASK-013 will remove the originals).
- pipe_body::~pipe_body() closes fd_ only if materialize() was never
  called (MHD owns it after a successful materialise).

New test:
- test/unit/body_test.cpp drives every subclass through MHD's
  daemon-independent inspection APIs (no daemon spun up). 12 tests, 29
  checks; the deferred trampoline is exposed as a public static so it
  can be unit-tested directly. Linked with explicit -lmicrohttpd
  (mirrors uri_log).

Observed sizes on libc++/arm64: empty=16, string=32, file=40, iovec=40,
pipe=16, deferred=40. All well under the 64 B SBO budget — TASK-010
will not need the heap-fallback branch on supported toolchains.

Out of scope (TASK-009/010): http_response wiring, body_inline_
fallback, kind() accessor, removal of v1 *_response subclasses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies fixes from the iter1 review pass on the detail::body hierarchy:

file_body (CWE-367 / perf):
- Open + fstat moved to constructor; size() is now accurate immediately.
- Drops lseek(SEEK_END); materialize() uses st_size from fstat.
  Closes the TOCTOU window between size discovery and the fd handed
  to MHD_create_response_from_fd, and removes the side-effect on the
  fd's read position.
- Adds destructor that closes fd_ only when MHD never took ownership
  (materialized_ stays false until from_fd returns non-null).

deferred_body (CWE-476):
- trampoline() guards against null cls and empty producer_ before
  invoking the std::function. MHD's callback path doesn't catch C++
  exceptions, so a bad_function_call would terminate in MHD's IO
  thread; the guard returns MHD_CONTENT_READER_END_WITH_ERROR instead.
- Constructor asserts producer_ is non-empty (debug-only precondition).

Header docs:
- file_body: documents path-canonicalisation contract (O_NOFOLLOW only
  blocks the final component) and fd ownership lifecycle.
- iovec_body: documents the borrowed-pointer lifetime contract
  (iov_base buffers must outlive the MHD_Response*) and the heap
  allocation note from DR-005.
- deferred_body: documents the std::function SBO caveat — capturing
  more than the implementation-defined threshold silently heap-allocates.

Tests:
- file_body_size_known_before_materialize: size() must be correct at
  construction (21 bytes for test_content), not only after materialize.
- deferred_body_trampoline_null_cls_returns_error: trampoline with
  cls==nullptr returns MHD_CONTENT_READER_END_WITH_ERROR rather than
  dereferencing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings in the polymorphic detail::body hierarchy plus iter1 review-pass
fixes (file_body TOCTOU, deferred_body null-callable guard, header
lifetime/ownership docs, and accompanying tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweeps in groundwork-generated planning content that had been left
untracked across recent task work, and adds .DS_Store to .gitignore so
macOS metadata stops appearing as untracked.

Planning content:
- specs/product_specs.md — top-level product spec.
- specs/architecture/ — system overview, architectural drivers,
  per-component specs (body-hierarchy, create-webserver, http-method,
  http-request, http-resource, http-response, route-table, webserver,
  websocket-handler), cross-cutting concerns, integration, feature
  availability, build/packaging, testing, observability, the DR-001..011
  decision records, open questions, documentation, and appendices.
- specs/tasks/M{1..6}-*/TASK-*.md — task definitions for the v2.0
  milestones (M1 foundation through M6 release). Pre-existing tasks
  TASK-006/007 were already tracked from prior commits; this adds the
  rest, including the M2 response, M3 request, M4 handlers, and M5
  routing-lifecycle definitions.

Review records:
- specs/unworked_review_issues/2026-04-30..2026-05-03_*.md — outputs
  from the iter1 review passes on TASK-001 through TASK-008. Captured
  for traceability; "unworked" denotes issues not yet folded back into
  task scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When TASK-003..008 were merged into feature/v2.0 they were not pushed
individually, so the cumulative push surfaced regressions across the
matrix. This sweeps them up.

Build error (basic ubuntu / valgrind / windows-IWYU):
- test/unit/body_test.cpp:56-60: static_cast<int>(uint8_t-enum) >= 0
  is always-true, breaking -Werror=type-limits. Replace with
  enumerator != body_kind::empty so the compile-time reference still
  guards against a missing enumerator without the bogus comparison.

cpplint (17 errors → 0):
- Include order:
  - src/details/body.cpp, src/iovec_response.cpp,
    src/httpserver/details/body.hpp,
    test/unit/{body_test,header_hygiene_test,http_method_test,
    iovec_entry_test}.cpp: move <microhttpd.h> and <sys/uio.h> into
    the C-system-header group so the layout is primary, c, c++, other.
- Missing includes:
  - src/details/body.cpp, src/iovec_response.cpp: add <string> for
    std::string in the file_body / iovec_response signatures.
  - src/iovec_response.cpp: add <utility> for std::move.
- Header guard:
  - src/httpserver/details/body.hpp: cpplint expects #ifndef GUARD as
    the first non-comment line. Move the SRC_HTTPSERVER_DETAILS_BODY_HPP_
    guard above the HTTPSERVER_COMPILATION #error block (which now
    lives inside the guard).
- Misc:
  - body_kind.hpp: NOLINT(build/include_what_you_use) on the `string`
    enumerator (cpplint mistook it for std::string).
  - body_test.cpp:251: split single-line if-with-multiple-statements.
  - http_method_test.cpp:121: add space between [] and { in lambda.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
etr and others added 30 commits May 9, 2026 19:12
Pure rename. The nine virtual member functions on http_resource are
now snake_case to match the public API style established by TASK-001:

  render_GET     -> render_get
  render_POST    -> render_post
  render_PUT     -> render_put
  render_DELETE  -> render_delete
  render_PATCH   -> render_patch
  render_OPTIONS -> render_options
  render_HEAD    -> render_head
  render_CONNECT -> render_connect
  render_TRACE   -> render_trace

The default render(...) fallback signature is unchanged.

Decision: this PR keeps the existing std::shared_ptr<http_response>
return type. The action item that calls for "http_response by value"
is the architectural turf of TASK-036, which already lists the dispatch
flip (modded_request::dhrs, internal_error_page, the auth path,
deferred-response lifetime) in its own action items and references the
already-renamed snake_case names in its acceptance criteria. Pulling
the value-return cutover into TASK-022 would force a transitional
shim because TASK-025/027/031 (which TASK-036 also depends on) have
not landed yet. Rename now; flip return type in TASK-036.

No compatibility shim. v2.0 is a clean break (PRD-NAM-REQ-001, §3.7,
§4.4, TASK-042). Subclasses still using the old camelCase names will
silently produce non-overriding shadow methods - that is the user's
signal to read the migration guide.

Acceptance:
- grep -E 'render_[A-Z]' src/httpserver/*.hpp returns empty
- Wider sweep (src/, test/, examples/, README.md, doc/) returns empty
- All 32 testsuite entries pass under release build
- All 32 testsuite entries pass under --enable-debug (-Werror -Wextra)
- cpplint clean on changed files

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update task status to Done in TASK-022.md and _index.md, check off the
completed action items, and record the 13 minor review findings (all
deferred or pre-existing) under specs/unworked_review_issues/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (RED)

Add test/unit/webserver_register_smartptr_test.cpp covering the
TASK-023 acceptance criteria as compile-time and runtime assertions:

  - Compile-time: new unique_ptr and shared_ptr overloads return void;
    raw-pointer overload is removed; bool family parameter is still
    accepted (TASK-024 owns the family-flag removal).
  - Runtime (acceptance): unique_ptr ownership transfer, dtor runs on
    webserver destruction, shared_ptr caller-retains-ref semantics.
  - Runtime (validation): null and duplicate registrations throw
    std::invalid_argument (replaces v1's silent `return false`).

Wire the new TU into test/Makefile.am check_PROGRAMS. The TU does not
build today: webserver still exposes only the raw-pointer overload.
This is the deliberate failing baseline for the API swap that follows.
Replace the raw-pointer register_resource overload with two
smart-pointer overloads expressing ownership at the call site:

  void register_resource(const std::string& path,
                         std::unique_ptr<T> res,
                         bool family = false);   // T derived from http_resource
  void register_resource(const std::string& path,
                         std::shared_ptr<http_resource> res,
                         bool family = false);

Why two:
  - unique_ptr: the webserver takes sole ownership; resource dtor runs
    on webserver destruction or unregister_resource. This matches the
    common case where the caller has no further use for the resource.
  - shared_ptr: caller and webserver share ownership; useful when the
    caller wants to mutate or query the resource after registration.
  - The unique_ptr overload is templated over the derived type so that
    `register_resource(path, std::make_unique<my_resource>())` resolves
    unambiguously without competing with the shared_ptr overload via
    an implicit unique-to-shared conversion.

Internal storage in webserver_impl now holds shared_ptr<http_resource>
in all three route-table maps and the route_cache_entry. The dispatch
path (finalize_answer) holds a shared_ptr copy across the rendering
call, closing a latent race where a concurrent unregister_resource
could invalidate a raw pointer mid-call.

Return type changes from bool to void. Duplicate registration now
throws std::invalid_argument (replaces v1's silent `return false`).
This is the safer interpretation: a moved-in unique_ptr would be
destroyed inside the conversion to shared_ptr before the duplicate
check runs, so a soft-fail return value would leave the caller with
no way to recover the resource. Throwing surfaces the failure.

Migration:
  - All 38 examples now use std::make_shared<resource_type>() at the
    call site. Where the local resource was used to call mutators
    (disallow_all, set_allowing) before registration, those calls now
    use ->.
  - Integration tests use a small as_shared() test-only helper that
    wraps a stack-local http_resource& in a shared_ptr with a no-op
    deleter, preserving the established "declare resource on stack,
    pass to register_resource" pattern across ~127 call sites.
  - The duplicate-detection tests in basic.cpp and ws_start_stop.cpp
    are rewritten to use LT_CHECK_THROW, asserting the new throw
    semantics.

Acceptance criteria:
  - `auto r = std::make_unique<my_resource>(); ws.register_resource(
    "/foo", std::move(r));` compiles and serves: covered by
    webserver_register_smartptr_test.cpp::unique_ptr_overload_compiles_and_serves.
  - The raw-pointer overload no longer exists in the public header:
    enforced by has_raw_register_resource SFINAE static_assert.
  - A test verifies the resource destructor runs when the webserver is
    destroyed: webserver_register_smartptr_test.cpp::
    unique_ptr_dtor_runs_on_webserver_destruction.
  - All 33 tests pass (release and debug builds).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds test/unit/webserver_register_path_prefix_test.cpp pinning both the
compile-time signature contract and the runtime matching semantics for
the new public API:

  - register_path / register_prefix exist with unique_ptr<T> and
    shared_ptr<http_resource> overloads, returning void.
  - unregister_path / unregister_prefix exist, returning void.
  - Negative SFINAE pin: the bool-family register_resource overloads
    (both unique_ptr and shared_ptr) must be removed (acceptance
    criterion #1, "grep ... bool" returns no results).
  - Runtime: register_prefix matches a longer URL; register_path does
    not; parameterized exact paths bind args; unregister_path /
    unregister_prefix / the umbrella unregister_resource alias all
    404 after removal; the [[deprecated]] register_resource forwarder
    still serves and behaves like register_path.

Wired into test/Makefile.am as the webserver_register_path_prefix
check_PROGRAMS entry.

Currently RED: register_path / register_prefix do not exist, and the
bool-family static_assert sentinels still see the old overloads. Phase
2 (GREEN) lands the implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the positional `bool family` parameter on register_resource
with a named-API choice between exact and prefix matching. Public
surface on `webserver`:

  - register_path(path, ptr)     -- exact-match registration. Both
                                    unique_ptr<T> and shared_ptr<http_resource>
                                    overloads, returning void. The
                                    unique_ptr<T> form is a templated
                                    inline shim that funnels into the
                                    shared_ptr overload (TASK-023's
                                    pattern) so derived types resolve
                                    unambiguously.
  - register_prefix(path, ptr)   -- prefix-match registration. Same
                                    overload shape.
  - register_resource(path, ptr) -- now [[deprecated]]; thin alias for
                                    register_path. The 3-arg `bool
                                    family` overloads are GONE.
  - unregister_path(path)        -- erase only an exact registration.
  - unregister_prefix(path)      -- erase only a prefix registration.
  - unregister_resource(path)    -- kind-agnostic convenience; calls
                                    both unregister_path and
                                    unregister_prefix (idempotent).

Implementation moves the validation/insertion logic into a private
register_impl_(path, res, family) helper so the family flag only lives
in one place. The same is done for unregister: unregister_impl_ builds
the http_endpoint with the correct family flag (so the erase actually
finds the original key) and only touches the registered_resources_str
fast-path map for non-family entries (the str map only ever held
exact registrations -- this fixes a latent erase bug where
unregister_resource called .erase(string) on a map<http_endpoint,...>).

The single_resource validation now reads "must be registered via
register_prefix" rather than "must be marked as family"; the
deprecated register_resource forwarder calls register_path, so a
single_resource caller doing register_resource("/", sp) now correctly
throws -- they must call register_prefix("/", sp).

Acceptance criteria:

  - `grep -E 'register_resource\([^)]+,\s*bool\s' src/httpserver/*.hpp`
    returns no results: the bool-family overloads are deleted.
  - The new test TU pins both kinds at runtime (prefix matches a
    longer URL; exact does not; parameterized exact path binds {id};
    unregister_* and the umbrella unregister_resource alias all 404
    after removal; the deprecated register_resource forwarder still
    serves and behaves like register_path).
  - All 34 testsuite entries pass under `make check -j1`.

Call sites updated in this commit:

  - examples/*: register_resource(p, ptr, true) -> register_prefix(p, ptr).
    Other examples (which used register_resource for exact match) are
    renamed to register_path to avoid -Werror on the new
    [[deprecated]] forwarder.
  - test/integ/*: same treatment. The smartptr unit test still
    exercises the deprecated forwarder (that is its purpose), so it
    silences -Wdeprecated-declarations file-wide.
  - examples/url_registration.cpp gains a "/family" prefix showcase
    and uses register_path / register_prefix throughout to model the
    new API surface for users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flip TASK-024 status to Done in both the per-task file and the tasks
index. Action items annotated with the decisions made:

  - register_resource is kept as a [[deprecated]] alias for
    register_path; the bool-family overload is removed.
  - unregister_resource is split into unregister_path /
    unregister_prefix, with unregister_resource kept as the
    kind-agnostic convenience.

No code changes; ready for review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ew issues

Security review on the v2.0 register_path/register_prefix split flagged a
TOCTOU window: the previous unregister_resource() called unregister_path()
and unregister_prefix() in sequence, each acquiring the lock independently,
so a concurrent reader could observe the route in only one map. Rewrite it
to compute both endpoint keys up front and hold a single unique_lock across
all four map erasures plus the route-cache clear. New unit test pins the
atomicity contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the eight-test compile-time + runtime suite for the new lambda
registration entry points: hello-world (PRD §3.4), per-method 405 +
Allow header, multi-method composition on a single path, all seven
overloads dispatch their method, duplicate (method, path) throws,
merge-then-conflict still throws, parameterized path binds {id}, and
class-resource registration on a lambda-owned path throws. Pins the
§4.7 detail::route_entry shape via static_assert.

Compiles will fail until on_get/post/put/delete/patch/options/head and
detail::route_entry land in GREEN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the public on_get / on_post / on_put / on_delete / on_patch /
on_options / on_head member functions on webserver, each accepting a
std::function<http_response(const http_request&)>. All seven forward
to a private webserver::on_method_ helper that:

- Builds (or merges into) a hidden detail::lambda_resource shim at the
  given path. The shim is a final subclass of http_resource holding
  one callable slot per http_method enumerator. It starts with every
  method disallowed (disallow_all()), so 405 dispatch falls through
  the existing finalize_answer mask path with no edit to the dispatch
  glue; on_* enables the matching bit per slot via set_allowing.
- Throws std::invalid_argument on conflict (slot already populated for
  the requested method, class-resource already registered at the path)
  and on bad input (empty handler, single_resource path violation).
- Inserts into the same three storage maps register_impl_ uses, so
  exact and parameterized lambda routes both land in the right tier
  (str fast-path map for exact non-parameterized; regex map for
  parameterized).

Adds detail::route_entry (architecture spec §4.7) holding method_set +
std::variant<lambda_handler, shared_ptr<http_resource>> + bool
is_prefix; pinned at compile time via static_assert in the test TU.
TASK-027 will plumb route_entry into a real 3-tier route table; until
then the lambda_resource shim is the dispatch carrier.

Both new headers live in src/httpserver/detail/ and are gated on
HTTPSERVER_COMPILATION; added to noinst_HEADERS so they're tarred but
never installed under $prefix/include.

35 tests pass (was 34); webserver_on_methods's eight runtime cases
plus its compile-time signature contract covers PRD §3.4 hello-world,
per-method 405 + Allow header, multi-method composition, all seven
overloads dispatch their method, duplicate (method, path) throws,
merge-then-conflict still throws, parameterized path binds {id}, and
class-resource registration on a lambda-owned path throws.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ping

Address validation findings from /groundwork:validate:
- Added 7 per-method dispatch tests, empty-handler guard test, and
  single-resource non-root path conflict test in webserver_on_methods_test.
- lambda_resource.hpp: replaced defensive null-slot fallback with
  assert(slot) + invariant comment; added <cassert>.
- webserver.cpp: block comment on the seven on_* forwarders.
- Marked TASK-025 Done in task spec and _index.md.
- Recorded unworked review issues (2 major, 38 minor) for follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds test/unit/webserver_route_test.cpp covering:

- Compile-time signature contract for both new public overloads:
  route(http_method, const string&, std::function<...>) and
  route(method_set, const string&, std::function<...>) returning void.
- Runtime curl tests for single-method route() (GET/POST), 405 with
  Allow header for unregistered methods, the headline acceptance test
  (load (method, path) pairs from a vector at runtime and dispatch
  via route()), method_set GET+HEAD both serve, method_set partial
  overlap with an existing handler throws atomically (POST stays
  unregistered), empty method_set throws, http_method::count_
  sentinel throws, duplicate / cross-overload conflicts with on_*
  throw, parameterized path binds through the regex tier, empty
  std::function throws on either overload.

Wires the new TU into test/Makefile.am (check_PROGRAMS +
webserver_route_SOURCES). Build of the test currently fails to
compile because webserver::route is missing -- this is the RED gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…points

Adds two public lambda-registration entry points to webserver:

  void route(http_method m,
             const std::string& path,
             std::function<http_response(const http_request&)> handler);

  void route(method_set methods,
             const std::string& path,
             std::function<http_response(const http_request&)> handler);

route() is the table-driven escape hatch for registering a handler
when the HTTP method is a runtime value (config-driven route tables,
programmatic registration loops). The on_* family is preferred when
the method is known statically. Both forms tunnel into the same
internal registration helper, now generalized:

- Renames the private webserver::on_method_(http_method, path, h)
  helper to webserver::on_methods_(method_set, path, h) so the
  atomic all-or-nothing multi-method registration semantics live in
  one place. The seven public on_* forwarders each wrap their
  http_method in method_set{}.set(m) before forwarding; route(http_method,
  ...) does the same after rejecting the http_method::count_ sentinel.
- on_methods_ pre-validates that EVERY requested slot is empty before
  mutating ANY of them. If any one would conflict (slot already taken
  on the existing lambda shim, or a class-based resource owns the path),
  no slot mutation happens and std::invalid_argument is thrown. This
  is what makes route(method_set{get, post}, "/p", ...) safe when GET
  is already taken on /p: the call throws AND POST stays unregistered.

The dispatch path (finalize_answer / lambda_resource shim) is unchanged.

Doc updates:
- New Doxygen block on the two route() declarations explicitly states
  the escape-hatch convention (per OQ-003 resolution).
- on_method_ -> on_methods_ rename ripples through three legacy comments
  in src/httpserver/detail/lambda_resource.hpp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ticks the four action-item checkboxes on TASK-026.md, flips the task's
own status line to "In Progress", and mirrors that status into the
M4 row of specs/tasks/_index.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The PIMPL split landed in dea9c63 / 369c2a8 and all action items are
checked. Status was held at "In Progress" to flag the 27 unworked
review findings recorded in
specs/unworked_review_issues/2026-05-04_115707_task-014.md. Those
findings remain deferred (tracked in that file); the task itself is
complete and downstream tasks (015–026) build on it.

Also sync specs/tasks/_index.md row to Done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All three are functionally complete in code and merged on the integration
branch. This commit only updates the status field to the canonical "Done"
value to keep tooling that reads the spec consistent.

- TASK-006: status was "Complete" (non-canonical phrasing). #define
  value-constants migrated to httpserver::constants; index row already
  said Done.
- TASK-007: status carried a forward-reference to TASK-020. With
  TASK-020 merged, the hygiene gate is in full-enforcement mode
  (HEADER_HYGIENE_STRICT ?= yes, XFAIL_TESTS line removed, public hpps
  free of backend includes). Index row already said Done.
- TASK-015: status was "In Progress" though all action items were
  checked. http_request PIMPL pointer is in place, detail/
  http_request_impl.hpp exists, and http_request.hpp no longer pulls
  <microhttpd.h>/<gnutls/gnutls.h>. Index row also synced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the architecture-mandated v2 route storage shape (§4.7)
alongside the v1 maps. The v2 tier table is populated atomically with
v1 by every register_path / register_prefix / on_methods_ /
unregister_* path, and a new webserver_impl::lookup_v2() walks the
pipeline (cache -> exact -> radix -> regex) with documented
table-BEFORE-cache lock order.

What ships:
- detail::radix_tree<T>: bespoke segment-trie supporting exact +
  parameterized + prefix routes with deepest-prefix-wins semantics.
- detail::route_cache: 256-entry LRU keyed on (method, path) under a
  plain std::mutex.
- webserver_impl::{exact_routes_, param_and_prefix_routes_,
  regex_routes_, route_cache_v2, route_table_mutex_} populated
  alongside v1 storage.
- webserver_impl::lookup_v2() with tier_hit instrumentation.
- webserver_test_access friend hook (HTTPSERVER_COMPILATION-gated)
  giving unit tests a thin path into impl_ without widening the
  public API; mirrors the SBO test access pattern.
- method_set::empty() helper (TASK-026 review item #3).
- New tests: route_table (radix + cache unit coverage),
  lookup_pipeline (tier-order + cache promotion pin), and
  route_table_concurrency (4 writers + 16 readers stress test;
  TSan is a documented manual rebuild gate in the file header).
- Architecture doc updated with explicit lock-order sentence.

Cycle K (cutting the dispatch site over to lookup_v2 and demolishing
the v1 storage) is intentionally deferred to a follow-up so this PR
stays purely additive and back-compat. The microbenchmark and TSan
CI matrix variant (plan §3.6 / §3.7) are documented manual gates and
similarly tracked as follow-ups outside TASK-027 scope.

39/39 tests pass under both release and -Werror -Wextra debug builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tier helper

Address validation findings from /groundwork:validate (2 passes):
- src/httpserver/detail/webserver_impl.hpp: regex_routes_ now holds
  pre-compiled std::regex objects in a regex_route struct (url_complete,
  compiled_re, entry). Architecture §4.7 specifies the regex tier as a
  vector of (compiled std::regex, route_entry), not (std::string,
  route_entry) — addressing architecture-alignment-checker.
- src/webserver.cpp: introduced classify_route_tier() helper as single
  source-of-truth for v2 tier placement (radix / regex / exact),
  replacing the duplicated tier-classification branches in
  register_impl_ and on_methods_ (code-simplifier finding). The
  on_methods_ update path is now fresh-gated to avoid redundant regex
  recompilation on subsequent on_post / on_get / etc. calls.
- Tests added: regex_route_hits_regex_tier and
  prefix_subpath_first_lookup_hits_radix_not_cache in
  lookup_pipeline_test; radix_tree_matches_multiple_parameterized_segments
  and cache_duplicate_insert_replaces_in_place_and_keeps_size in
  route_table_test; on_get_and_on_post_compose_on_true_regex_path in
  webserver_on_methods_test; concurrency-test extensions in
  route_table_concurrency.
- Minor comment cleanup in radix_tree.hpp.
- Marked TASK-027 Done in task spec and _index.md.
- Recorded unworked review issues (2 runs: 10 findings then 62 findings;
  follow-ups for v2 dispatch cutover, heap allocations on lookup hot
  path, end-to-end captured-params test, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces v1's three maps with the architecture-mandated 3-tier shape
(§4.7): unordered_map for exact, radix tree for parameterized + prefix,
regex chain for fallback, all behind a 256-entry LRU cache. The v2
storage is populated alongside v1; lookup_v2() exists with cache →
exact → radix → regex pipeline and tier_hit instrumentation. Cycle K
(cutting the dispatch site over to lookup_v2 and demolishing v1) is
deferred to a follow-up so this merge stays additive and back-compat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the v2.0 routing-semantics regression gate per AR-003. The v1
routing corpus has already been API-ported (TASK-023..026) and runs
through v1 dispatch maps. This task pins the v2 3-tier table semantics
ahead of TASK-036's dispatch cutover, so any divergence shows up as a
release-blocker before users see a regression.

New test TU `test/unit/routing_regression_test.cpp`: 17 LT_AUTO_TESTs,
one per row of the pattern taxonomy in test/REGRESSION.md, each
driving the public registration surface and probing webserver_impl::
lookup_v2 via webserver_test_access. Covers exact / root-only /
parameterized (1, 2, custom-regex) / prefix / prefix-shadowed-by-exact
/ pure-regex / register-unregister cycles / on_* method-set composition
/ overlapping precedence / single-resource mode / no_regex_checking.

Implementation fix: lookup_v2 now canonicalizes incoming paths via
canonicalize_lookup_path (strip trailing /, prepend leading /),
matching the registration-side normalization in http_endpoint and the
v1 dispatch path. Cache key uses the canonical form too.

Documented v2 divergences (test/REGRESSION.md):
- Per-segment regex constraints (/{id|([0-9]+)}) are NOT enforced by
  the radix tier; must land before TASK-036 cuts dispatch over.
- Overlapping-route precedence is deterministic structural order
  rather than v1's std::map iteration accident; test pins v2 behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address validation findings from /groundwork:validate (2 passes):
- test/unit/routing_regression_test.cpp: fix always-green assertion in
  the overlapping-routes test (was *hp == first || *hp == second);
  add unregistered_path_yields_miss as the pure-miss baseline; add
  second_lookup_same_path_hits_cache_tier and
  unregister_invalidates_cache_entry to pin the LRU cache behaviour
  the gate's own rationale calls out; add FIXME(TASK-036-prereq)
  comment on the non_numeric assertions documenting the v1/v2 custom-
  regex divergence.
- specs/tasks/_index.md: normalise TASK-028 status from Not Started to
  Done — the row was stale before this branch.
- Recorded unworked review issues (31 minor findings: REGRESSION.md
  prose updates, naming/style nits, doc tweaks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pins the v1 routing-test corpus against the new 3-tier table as the
release-blocker gate (AR-003, §9 testing item 5). Adds
test/unit/routing_regression_test.cpp covering all six v1 taxonomy
rows (exact, parameterized single/multi-segment, prefix, regex,
method-mismatched) plus pure-miss baseline, cache-tier hit, and cache
invalidation on unregister. Documents the corpus in test/REGRESSION.md
including the v2 lookup-canonicalisation fix in webserver.cpp
(handle_request now strips trailing slash + query/fragment before
lookup) and the one explicit divergence (custom regexes — pinned with
FIXME(TASK-036-prereq)). Wired into make check via test/Makefile.am.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant